iT邦幫忙

2022 iThome 鐵人賽

DAY 25
0
Security

我逆向你逆向我的逆向工程膩系列 第 25

Dx25 - 寫一個 API Hook 程式

  • 分享至 

  • xImage
  •  

這次的目標是寫一個能利用 API Hook 來注入自訂功能的工具,我選用了 Debug API 這個方法以除錯法的方式來進行注入,那先看看成果如何 :

本程式在 x64 Windows 10 確認可以執行。
https://github.com/Dinlon5566/IT_Reverse_Engineering/tree/main/Dx25

預覽

  1. 打開 notepad.exe 並隨意打字並存起來 ( 防止掛勾後無法新增檔案 )。
  2. 利用這次寫的工具 apiHooker.exe 來對記事本進行 API Hook。
  3. 在記事本寫上文字,之後進行存檔。
  4. 看到 WriteFile() 被調用,把他傳入的文字修改成全部大寫。

https://ithelp.ithome.com.tw/upload/images/20221009/20135675KUrOMDZWlk.png

  1. 開啟檔案,文字全部變成大寫

https://ithelp.ithome.com.tw/upload/images/20221009/20135675Ijs7U5fsEf.png

如果存檔時發現記事本無回應,請立即使用工作管理員把記事本關閉。否則會一直寫入錯誤的記憶體區間導致卡死。這是因為新增檔案的時候並不是使用 WriteFile(),插入的 Hook 未取出所導致的記憶體存取錯誤

構築

那先來看看,這個工具實際上要如何製作呢 ? 首先先看簡單的流程 :

  1. main() 對參數進行檢查,之後來到 StayDebugEvent() 等待事件發生
  2. 若是第一次進入程序進行 doCreateEvent()
    1. 取得 API 位置
    2. 透過上 API 位置來得到 WriteFile() 位置
    3. 計算修改後的位置,此處直接將第一個 BYTE 改成 0xCC。當程序碰到 0xCC 時會觸發 INT3 例外,由此把程序交到 Deubger (注入器) 手上
  3. 若出現事件則進入 doExceptionEvent()
    1. 若為寫入事件則處理程序,否則跳回 StayDebugEvent()
    2. 接下來是處裡 API Hook 的很多東西,等等慢慢講
  4. 一直等待事件發生,直到目標程式退出。

https://ithelp.ithome.com.tw/upload/images/20221009/201356751RiLAumsKI.png

那我們開始吧,程式碼裡的註解說明了指令的功能與目標 :

  1. 首先,main() 的部分像這樣。

    先把參數轉為數字做為 PID,利用 DebugActiveProcess() 來檢查目標程序是否存在。

int main(int argc, char* argv[]) {
	if (argc != 2) {
		printf("USEAGE : apiHooker.exe <PID> \n");
		// 這一行方便我取用 notepad.exe 的 PID
		system("tasklist | findstr notepad");
		return 1;
	}

	DWORD dwPID = atol(argv[1]);
	if (!dwPID) {
		printf("PID error!");
		return 0;
	}
	printf("--- PID : %d ---\n", dwPID);
// 檢查是否程序是否存在
	if (!DebugActiveProcess(dwPID)) {
		printf("Debug Fail\n");
		printf("Error code : %d", GetLastError());
		return 1;
	}
	StayDebugEvent();
	return 0;

}
  1. 等待事件的地方,當有事件時 WaitForDebugEvent 就會把事件發到 debugEvent,透過比對 EventCode 來判斷事件類型。之後並把 debugEvent 作為媒介發給 doCreateEvent()doExceptionEvent() 方便之後直接利用。

    當一圈跑完之後需要 Continue 讓事件後的指令繼續跑下去。

BOOL StayDebugEvent() {

	DEBUG_EVENT debugEvent;
	DWORD dwStat;
// 等待事件發生
	while (WaitForDebugEvent(&debugEvent, INFINITE)) {
		if (debugEvent.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) {
			doCreateEvent(&debugEvent);
		}
		else if (debugEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT)
		{
			doExceptionEvent(&debugEvent);
		}
		else if (debugEvent.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) {
			printf("Process %d is down!\n", debugEvent.dwProcessId);
			break;
		}
	 // 繼續跑下去
		ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
	}

	return 1;
}
  1. 這邊很長,不過其實都很好理解

首先這邊是宣告的變數

// 用來存 WriteFile 的位置
LPVOID pfWriteFile = NULL;
// 放置 CREATE_PROCESS 時的資料(來自於debugEvent->u.CreateProcessInfo)
CREATE_PROCESS_DEBUG_INFO debugInfo;
// 用於放置替換資料與備份資料 (第一個 BYTE),0xCC藉此觸發INT3斷點異常
BYTE chINT3 = 0xCC, chOrgByte = 0;

看到 doCreateEvent :

主要用於把函式庫的位置記下來,並把 Hook 接上去。使 0xCC INT3 例外發生時就會觸發事件。

BOOL doCreateEvent(LPDEBUG_EVENT debugEvent) {
	printf("--- CreateEvent START ---\n");

	//取得函式庫與得到 WriteFile API 的位置
	HMODULE module = GetModuleHandleA("kernel32.dll");
	pfWriteFile = GetProcAddress(module, "WriteFile");

	//輸出
	printf("kernel32.dll -> %p\n", module);
	printf("\\->WriteFile -> %p\n", pfWriteFile);

	// 將 debugEvent 的 CreateProcessInfo 寫到 debugInfo,u.CreateProcessInfo 的結構為 CREATE_PROCESS_DEBUG_INFO。
	// https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-debug_event
	memcpy(&debugInfo, &debugEvent->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));

	
	// 讀入目標程序的 pfWriteFile 位置。
	BYTE arr[10];
	ReadProcessMemory(debugInfo.hProcess, pfWriteFile, arr , sizeof(BYTE)*10, NULL);
	// 用來還原 API 位置
	chOrgByte = arr[0]; 

	printf("API Address : ");
	for (BYTE i : arr) {
		printf("%02X", i);
	}

	// 將第一個 BYTE 改成 0xCC 接上 Hook
	WriteProcessMemory(debugInfo.hProcess, pfWriteFile, &chINT3, sizeof(BYTE), NULL);

	// 讀入更改後的WriteFile並顯示 (其實不需要這個步驟)
	ReadProcessMemory(debugInfo.hProcess, pfWriteFile, arr, sizeof(arr), NULL);
	printf("\n           -> ");
	for (BYTE i : arr) {
		printf("%02X", i);
	}

	printf("\n--- CreateEvent DONE ---\n");
	return 1;
}
  1. 然後是處裡事件的地方
BOOL doExceptionEvent(LPDEBUG_EVENT debugEvent) {

	CONTEXT context;
	PBYTE pBuf = NULL;
	ULONG_PTR ulpWriteLen, ulpBufAddress;
	PEXCEPTION_RECORD64 debugRecord = (PEXCEPTION_RECORD64)&debugEvent->u.Exception.ExceptionRecord;
	BYTE arr[10];

	// 確認 Exception 的位置為原本的 WriteFile 位置,正是設立INT3中斷的位置。
	if (debugRecord->ExceptionCode == EXCEPTION_BREAKPOINT &&
		debugRecord->ExceptionAddress==(DWORD64)pfWriteFile
		) {
		printf("--- Find WriteFile Called! ---\n");
		printf("APIAddress : %p\n",(DWORD64)pfWriteFile);
		
		// 把之前改過的 API 位置改回去( 取走 Hook )
		WriteProcessMemory(debugInfo.hProcess, pfWriteFile, &chOrgByte, sizeof(BYTE), NULL);

		// 輸出 API 位置
		ReadProcessMemory(debugInfo.hProcess, pfWriteFile, arr, sizeof(arr), NULL);
		for (BYTE i : arr) {
			printf("%02X", i);
		}
		printf("\n");

在取得我們輸入的資料的時候,必須得到暫存器中的值用於獲得 notepad.exe 傳遞給 WriteFile() 的參數,可以透過以下來查看暫存器的資料

		// 取得 Register
		context.ContextFlags = CONTEXT_FULL;
		GetThreadContext(debugInfo.hThread,&context);

		// 輸出 Register,方便查看
		printf("\nRegister data :\n");
		printf("RAX :%p\n", context.Rax);
		printf("RBX :%p\n", context.Rbx);
		printf("RCX :%p\n", context.Rcx);
		printf("RDX :%p\n", context.Rdx);
		printf("R8  :%p\n", context.R8);

這邊直接指明 RDX 是存放字串的地方,而 R8 傳遞了資料的長度。

https://ithelp.ithome.com.tw/upload/images/20221009/20135675SSLwKmtEpA.png

可以透過查看檔案來確認 28 為文件大小 :

https://ithelp.ithome.com.tw/upload/images/20221009/20135675Jhe0Rudy4r.png

接下去


		// 將傳遞值由寄存器撈上來
		ulpBufAddress = context.Rdx;
		ulpWriteLen = context.R8;

		// 宣告緩衝區空間與清空
		pBuf = (PBYTE)malloc(ulpWriteLen+1);
		memset(pBuf,0,ulpWriteLen+1);

		// 讀取文字位置的內容到緩衝區
		ReadProcessMemory(debugInfo.hProcess,(LPVOID)ulpBufAddress,pBuf,ulpWriteLen,NULL);
		printf("Org String :\n%s\n",pBuf);
		
		// 把字都變成大寫
		for (unsigned int i = 0; i < (unsigned int)ulpWriteLen; i++) {
			if ('a' <= pBuf[i] && pBuf[i] <= 'z') {
				pBuf[i] -= 0x20;
			}
		}

		printf("Aft String :\n%s\n", pBuf);
		
		// 將緩衝區內容存到原本放字串的位置
		WriteProcessMemory(debugInfo.hProcess,(LPVOID)ulpBufAddress,pBuf,ulpWriteLen,NULL);

		// 緩衝區沒用了,釋放掉
		free(pBuf);
		
		// 把執行位置調到 WriteFile 的位置
		context.Rip = (DWORD64)pfWriteFile;
		// 然後把 Context 的資料設置給 Thread
		SetThreadContext(debugInfo.hThread,&context);

		// 原程序可以繼續行動了
		ContinueDebugEvent(debugEvent->dwProcessId,debugEvent->dwThreadId,DBG_CONTINUE);
		
		// 把 WriteFile 的第一 BYTE 改回 0xCC ( 把 Hook 放回去 )
		WriteProcessMemory(debugInfo.hProcess, pfWriteFile, &chINT3, sizeof(BYTE),NULL);
		
		printf("-- - Write Change end-- - \n");
	}
	return 1;
}

這篇主要是在程式中學習如何利用除錯法來進行 API HOOK,在使用 DEBUG API 寫了這份程式後就會發現其實看起來有很多東西,不過實際上流程是很明確的。但是在編輯的時候記憶體控制要很精確,不然就會發生這種東西 :

https://ithelp.ithome.com.tw/upload/images/20221009/2013567503j5Dob7Zm.png

對。快 6 GB 的文件檔,差點把電腦搞壞 : /

寫這種東西時討厭的是因為系統架構不同 ( 像是 XP / Win7 / Win10 ),導致以前的代碼無法在新架構上使用,都要到各個地方查資料很花時間。

資料來源 :
https://www.codeproject.com/Articles/43682/Writing-a-basic-Windows-debugger
https://www.cnblogs.com/UnGeek/p/3515995.html
https://www.cnblogs.com/whitehawk/p/11166557.html
https://www.apriorit.com/dev-blog/727-win-guide-to-hooking-windows-apis-with-python


上一篇
Dx24 - API Hook
下一篇
Dx26 - ASLR 保護技術 & 去除 ASLR
系列文
我逆向你逆向我的逆向工程膩31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言